Moveable/draggable <div>
Asked Answered
A

9

65

This is my updated and modified script, it works completely, except I would like to universalize it... observe the **** how can I make it so that I don't have to do function(e){BOX.Draggable.elemen = e.target || e.srcElement; elementDraggable(e); everytime I need to use the dragable function for a different element?

window.onload = addListeners;

var BOX = function(){
  return{
    Draggable: function(){}
  };
}();

function addListeners(){
  document.getElementById('div').addEventListener('contextmenu', menumove, false);
  **document.getElementById('div').addEventListener('mousedown', function(e){BOX.Draggable.elemen = e.target || e.srcElement; elementDraggable(e);}, false);**
}

function elementDraggable(e){
  var e = e || window.event;
  var div = BOX.Draggable.elemen;
  BOX.Draggable.innerX = e.clientX + window.pageXOffset - div.offsetLeft;
  BOX.Draggable.innerY = e.clientY + window.pageYOffset - div.offsetTop;

  window.addEventListener('mousemove', elementMove, false);
  window.addEventListener('mouseup', function(){
    window.removeEventListener('mousemove', elementMove, false);
    }, true);

  function elementMove(e){
    div.style.position = 'absolute';
    div.style.left = e.clientX + window.pageXOffset - BOX.Draggable.innerX + 'px';
    div.style.top = e.clientY + window.pageYOffset - BOX.Draggable.innerY + 'px';
  }
}
Aksel answered 17/2, 2012 at 19:21 Comment(1)
This post has a much simpler JS version #24051238Rubadub
O
76

Is jQuery an option for you? It makes what you are doing really simple since the code already exists.

http://jqueryui.com/demos/draggable/

Demo

JavaScript Code

window.onload = addListeners;

function addListeners(){
    document.getElementById('dxy').addEventListener('mousedown', mouseDown, false);
    window.addEventListener('mouseup', mouseUp, false);

}

function mouseUp()
{
    window.removeEventListener('mousemove', divMove, true);
}

function mouseDown(e){
  window.addEventListener('mousemove', divMove, true);
}

function divMove(e){
    var div = document.getElementById('dxy');
  div.style.position = 'absolute';
  div.style.top = e.clientY + 'px';
  div.style.left = e.clientX + 'px';
}​
Omniscience answered 17/2, 2012 at 19:23 Comment(9)
I already know how to do this with jquery, I am trying to learn how to do it manually with javascript, thanks.Aksel
yea, it didn't save the latest i i just realized it. I'm rewriting itOmniscience
Ok. it should be fine now. I can't believe it didn't update it the first time and lost it.Omniscience
Thanks, I had already figured out most of it, the only part I didn't have was the removeEventListener on mouseup part.Aksel
I noticed that the mouse "jumps" to the upper left corner of the div when dragging it - how can I fix this?Cranwell
@AndersonGreen You have to take into account the position where the drag starts. If you start dragging from the middle of the div you have to offset the position by that. See jsfiddle.net/wfbY8/737 (old comment I know, but needing a reply..)Biome
jquery ui is very heavy! you should not install a full library for one simple thing. i kno that jquery ui is easy to use but we as developers have to start thinking in terms of "small".. especially for mobile.Hord
That's true MrRioku but if he was already loading the library that would have been his best option. He wasn't so I provided him with straight JavaScript code.Omniscience
Is there any reason why the functions mouseDown and mouseUp have the useCapture parameter set to true when attaching events? While the addListeners function use false when attaching events?Galba
S
67

This is a nice no-jQuery script to drag a div: http://jsfiddle.net/g6m5t8co/1/

var mydragg = function() {
  return {
    move: function(divid, xpos, ypos) {
      divid.style.left = xpos + 'px';
      divid.style.top = ypos + 'px';
    },
    startMoving: function(divid, container, evt) {
      evt = evt || window.event;
      var posX = evt.clientX,
        posY = evt.clientY,
        divTop = divid.style.top,
        divLeft = divid.style.left,
        eWi = parseInt(divid.style.width),
        eHe = parseInt(divid.style.height),
        cWi = parseInt(document.getElementById(container).style.width),
        cHe = parseInt(document.getElementById(container).style.height);
      document.getElementById(container).style.cursor = 'move';
      divTop = divTop.replace('px', '');
      divLeft = divLeft.replace('px', '');
      var diffX = posX - divLeft,
        diffY = posY - divTop;
      document.onmousemove = function(evt) {
        evt = evt || window.event;
        var posX = evt.clientX,
          posY = evt.clientY,
          aX = posX - diffX,
          aY = posY - diffY;
        if (aX < 0) aX = 0;
        if (aY < 0) aY = 0;
        if (aX + eWi > cWi) aX = cWi - eWi;
        if (aY + eHe > cHe) aY = cHe - eHe;
        mydragg.move(divid, aX, aY);
      }
    },
    stopMoving: function(container) {
      var a = document.createElement('script');
      document.getElementById(container).style.cursor = 'default';
      document.onmousemove = function() {}
    },
  }
}();
#container {
  position: absolute;
  background-color: blue;
}

#elem {
  position: absolute;
  background-color: green;
  -webkit-user-select: none;
  -moz-user-select: none;
  -o-user-select: none;
  -ms-user-select: none;
  -khtml-user-select: none;
  user-select: none;
}
<div id='container' style="width: 600px;height: 400px;top:50px;left:50px;">
  <div id="elem" onmousedown='mydragg.startMoving(this,"container",event);' onmouseup='mydragg.stopMoving("container");' style="width: 200px;height: 100px;">
    <div style='width:100%;height:100%;padding:10px'>
      <select id=test>
        <option value=1>first
          <option value=2>second
      </select>
      <INPUT TYPE=text value="123">
    </div>
  </div>
</div>
Scarabaeus answered 19/9, 2014 at 12:7 Comment(6)
Great solution! It's quite generic and also makes sure you cannot move the div outside the parent container!Cretic
Yep great example! Another issue with it is that if you start dragging and move cursor outside the green box mouse up does not register....in other words mouse up event does not register outside the green box.Whence
Davvit, add 'onmouseout' for container.Related
What's the use of the var a = document.createElement('script'); line in the stopMoving function?Fourscore
this is way simpler: #24051238Rubadub
I really appreciate your code! Currently I'm using #24051238 which I think is a simpler way to start and learn this. Next, inspired by yours, I want to block dragging past boundaries.Seftton
T
15

After trying jnoreiga's accepted answer, I found it very annoying that the dragged element abruptly snapped to the top left corner, rather than maintaining the same relative position.

This snippet prevents the awkward aforementioned behavior via an offset, and provides a simple interface to create draggable elements one at a time or en masse via a forEach call or similar.

window.onload = function() {
  draggable(document.getElementById('foo'));
}

function draggable(el) {
  el.addEventListener('mousedown', function(e) {
    var offsetX = e.clientX - parseInt(window.getComputedStyle(this).left);
    var offsetY = e.clientY - parseInt(window.getComputedStyle(this).top);
    
    function mouseMoveHandler(e) {
      el.style.top = (e.clientY - offsetY) + 'px';
      el.style.left = (e.clientX - offsetX) + 'px';
    }

    function reset() {
      window.removeEventListener('mousemove', mouseMoveHandler);
      window.removeEventListener('mouseup', reset);
    }

    window.addEventListener('mousemove', mouseMoveHandler);
    window.addEventListener('mouseup', reset);
  });
}
/* The only required styling is position: absolute */
#foo {
  position: absolute;
  border: 1px solid black;
  overflow: hidden;
}

/* Prevents inconsistent highlighting of element while being dragged
   Copied from https://stackoverflow.com/questions/826782 */
.noselect {
    -webkit-user-select: none; /* Safari */
            user-select: none; /* Every other browser */
}
<div id="foo" class="noselect">This is a draggable div!</div>
Thickwitted answered 21/5, 2019 at 21:7 Comment(2)
This is really easy to follow and use. The only change I made in 2024 was to switch everything to pointer eventsPrado
As of January 2020, MDN says user-select compatibility is universal except for Safari, so you can just do -webkit-user-select:none; user-select:noneLownecked
T
13

I modified Shaedo's code a little bit, wraps it in a function and add a feature that you can drag an element by only parts of it or its children, say the title bar of a div. Note in this demo, you can only drag the red area to move the blue area.

function makeDragable(dragHandle, dragTarget) {
  // used to prevent dragged object jumping to mouse location
  let xOffset = 0;
  let yOffset = 0;

  let handle = document.querySelector(dragHandle);
  handle.addEventListener("mousedown", startDrag, true);
  handle.addEventListener("touchstart", startDrag, true);

  /*sets offset parameters and starts listening for mouse-move*/
  function startDrag(e) {
    e.preventDefault();
    e.stopPropagation();
    let dragObj = document.querySelector(dragTarget);
    
    // shadow element would take the original place of the dragged element, this is to make sure that every sibling will not reflow in the document
    let shadow = dragObj.cloneNode();
    shadow.id = ""
    // You can change the style of the shadow here
    shadow.style.opacity = 0.5
    dragObj.parentNode.insertBefore(shadow, dragObj.nextSibling);

    let rect = dragObj.getBoundingClientRect();
    dragObj.style.left = rect.left;
    dragObj.style.top = rect.top;
    dragObj.style.position = "absolute";
    dragObj.style.zIndex = 999999;

    /*Drag object*/
    function dragObject(e) {
      e.preventDefault();
      e.stopPropagation();
      if(e.type=="mousemove") {
        dragObj.style.left = e.clientX-xOffset + "px"; // adjust location of dragged object so doesn't jump to mouse position
        dragObj.style.top = e.clientY-yOffset + "px";
      } else if(e.type=="touchmove") {
        dragObj.style.left = e.targetTouches[0].clientX-xOffset +"px"; // adjust location of dragged object so doesn't jump to mouse position
        dragObj.style.top = e.targetTouches[0].clientY-yOffset +"px";
      }
    }
    /*End dragging*/
    document.addEventListener("mouseup", function() {
      // hide the shadow element, but still let it keep the room, you can delete the shadow element to let the siblings reflow if that is what you want
      shadow.style.opacity = 0
      shadow.style.zIndex = -999999
      window.removeEventListener('mousemove', dragObject, true);
      window.removeEventListener('touchmove', dragObject, true);
    }, true)

    if (e.type=="mousedown") {
      xOffset = e.clientX - rect.left; //clientX and getBoundingClientRect() both use viewable area adjusted when scrolling aka 'viewport'
      yOffset = e.clientY - rect.top;
      window.addEventListener('mousemove', dragObject, true);
    } else if(e.type=="touchstart") {
      xOffset = e.targetTouches[0].clientX - rect.left;
      yOffset = e.targetTouches[0].clientY - rect.top;
      window.addEventListener('touchmove', dragObject, true);
    }
  }
}

makeDragable('#handle1', '#moveable1')
makeDragable('#handle2', '#moveable2')
makeDragable('#handle3', '#moveable3')
.moveable {
    width: 100px;
    height: 100px;
    background: blue;
    display: inline-block
}

.handle {
    width: 100px;
    height: 20px;
    cursor: move;
    background: red;
}

#moveable2 { background: green }
#moveable3 { background: teal }
<div id="moveable1" class="moveable">
    <div id="handle1" class="handle">
    </div>
</div>
<div id="moveable2" class="moveable">
    <div id="handle2" class="handle">
    </div>
</div>
<div id="moveable3" class="moveable">
    <div id="handle3" class="handle">
    </div>
</div>
Tega answered 1/12, 2017 at 15:0 Comment(1)
This should definitely be the accepted answer: generic in it's approach, easy to use and even supports touch events. Oh, and it takes into account that often the container to drag will be different from the handle you're dragging it with. Thanks a lot!! :)Journalistic
S
12

Well, your movement code simplifies to:

div.style.position = "absolute";
div.style.top = e.clientY - (e.clientY - div.offsetTop) + "px";
div.style.left = e.clientX - (e.clientX - div.offsetLeft) + "px";

Basic math here - the e.clientX and e.clientY have absolutely no effect on the position here, so you're just taking the offsetLeft and reassigning it to the style.left, and the same for the top. Thus no movement whatsoever.

What you need to do is save the clientX and clientY when the mousedown happens, and do the subtraction based on that.

Oh and you're also setting the event listener wrong. The way it is now, you have it run divMove once and the return value (undefined here) is the function attached as the listener. Instead, use function(e) {divMove(dxy,e || window.event);}.

Stalky answered 17/2, 2012 at 19:29 Comment(4)
I mostly understand what you're saying and I can see that you're right. But i'm not exactly sure how to change it to what you're saying. could you put up a snippet to show me exactly what you mean?Aksel
You said in a comment to the other answer that you're trying to learn, so I won't do it for you, but basically you have two main ways of doing it. 1) Use absolute positioning, get the current mouse coordinates and set the element to those coordinates. 2) Use relative positioning, and take the difference between the current position and the start position.Stalky
Thanks, for making me do it on my own. I was able to figure out almost everything, and just needed a little bit of help because I didn't know about the removeEventListener method :-)Aksel
Yay ^_^ My own drag-and-drop code just uses elem.onmousemove, setting it to null instead of removeEventListener. Personal preference.Stalky
S
2

For all my friends:

    function initDrag(div) {
        div.addEventListener('mousedown', e => {
            if (e.target === div){
                startDrag(e, div);
            };
        });
    };

    function startDrag(e, div) {

        const offsetX = e.offsetX;
        const offsetY = e.offsetY;

        const controller = new AbortController();

        div.style.cursor = "grabbing";

        document.addEventListener('mousemove', e => {
            div.style.top = window.scrollY + e.clientY - offsetY + 'px';
            div.style.left = window.scrollX + e.clientX - offsetX + 'px';
        }, { signal: controller.signal }, true);

        'mouseup,blur'.split(',').forEach(e => {
            document.addEventListener(e, () => {
                controller.abort();
                div.style.cursor = "grab";
            }, { once: true }, true);
        });
    };
Shrubby answered 20/11, 2021 at 2:37 Comment(0)
P
2

This is modified @evan answer, it add handle

function draggable(container, handle) {
    let movable = handle ? handle : container;
    ['mousedown', 'touchstart'].forEach(event => {
        movable.addEventListener(event, e => {
            var offsetX = e.clientX - parseInt(getComputedStyle(container).left);
            var offsetY = e.clientY - parseInt(getComputedStyle(container).top);

            function mouseMoveHandler(e) {
                container.style.top = (e.clientY - offsetY) + 'px';
                container.style.left = (e.clientX - offsetX) + 'px';
            }

            function reset() {
                removeEventListener('mousemove', mouseMoveHandler);
                removeEventListener('mouseup', reset);
            }

            addEventListener('mousemove', mouseMoveHandler);
            addEventListener('mouseup', reset);
        });
    })
}

// box only
let boxOnly = document.querySelector('#boxOnly');
draggable(boxOnly);

// box with handle
let container = document.querySelector('#foo');
let header = document.querySelector('#foo .header')
draggable(container, header);
/* The only required styling is position: absolute */
#foo, #boxOnly {
  position: absolute;
  border: 1px solid black;
  user-select: none;
}
#boxOnly{left: 300px;}
.header { background: orange;margin: 0;text-align:center;  padding: 6px;}
p.header:active, div#boxOnly:active {cursor:move;}
<div id="foo">
  <p class="header">Header</p>
  <p>Content cannot be dragged, <br>use header</p>
</div>

<div id="boxOnly">
  <p>Content can be dragged</p>
</div>
Price answered 16/8, 2022 at 9:8 Comment(0)
B
1

Any solution that uses clientY, clientX, pageY, or pageX within the dragend event will completely fail in Firefox. Source: Bugzilla: Bug #505521, Set screen coordinates during HTML5 drag event.

How do we get by this limitation? document's drop event also fires at the same exact time as the dragend event of the dragged element. But, we can see things like clientY and clientX here in firefox. So, let's just use that.

Two working demos, 100% JavaScript-Only Solution: SO Code Snippet and JSBin.

var startx = 0;
var starty = 0;
dragStartHandler = function(e) {
  startx = e.clientX;
  starty = e.clientY;
}

dragOverHandler = function(e) {
  e.preventDefault();
  return false;
}

dragEndHandler = function(e) {
  if(!startx || !starty) {
    return false;
  }
  
  var diffx = e.clientX - startx;
  var diffy = e.clientY - starty;
  
  var rect = e.target.getBoundingClientRect();

var offset = { 
                top: rect.top + window.scrollY, 
                left: rect.left + window.scrollX, 
            };
  
  var newleft = offset.left + diffx;
  var newtop = offset.top + diffy;
  
  e.target.style.position = 'absolute';
  e.target.style.left = newleft + 'px';
  e.target.style.top = newtop + 'px';
  
  startx = 0;
  starty = 0;
}

document.getElementsByClassName("draggable")[0].addEventListener('dragstart', dragStartHandler);

document.addEventListener('dragover', dragOverHandler);
document.addEventListener('drop', dragEndHandler);
.draggable {
  border: 1px solid black;
  cursor: move;
  width:250px;
};
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  
  <BR><BR><BR>

  <div id="draggable1" class="draggable" draggable="true">
    Hey, try to drag this element!
  </div>
  
</body>
</html>

Explanation:

  • dragStartHandler() : This is bound to the draggable element. Here, all we do is record the current x/y coordinates at start.
  • dragOverHandler() : This is bound to the document, so that we can override the default dragover behavior. This is needed to do any type of drag & dropping.
  • dragEndHandler() : This is bound to the document's drop. Normally, we would want this to bind to the element's dragend, but since clientY and clientX are missing, we bind it to the document. This just does exactly what you'd want to happen when dragend is called, except you have x/y coordinates.

The formula used is:

set style to: (current position) - (original position)

That is as complicated as it gets, but to calculate and apply the style, just for the x-dimension, the code is...

var diffx = e.clientX - startx;
var rect = e.target.getBoundingClientRect();
var offset = { 
        left: rect.left + window.scrollX, 
    };
var newleft = offset.left + diffx;
e.target.style.position = 'absolute';
e.target.style.left = newleft + 'px';
Belshin answered 11/8, 2020 at 20:32 Comment(0)
V
0

An additional method to "niente00" code.

init : function(className){
    var elements = document.getElementsByClassName(className);
    for (var i = 0; i < elements.length; i++){
        elements[i].onmousedown = function(){mydragg.startMoving(this,'container',event);};
        elements[i].onmouseup = function(){mydragg.stopMoving('container');};
        }
    }
Venose answered 12/12, 2016 at 13:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.